package labrom.usage;

import java.util.UUID;

import android.app.Service;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ServiceInfo;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;

public class UsageDataService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener {
	



	public static final String PREF_KEY_GUID = "labrom.usage.UsageDataService.guid";
	public static final String PREF_KEY_ENABLE = "labrom.usage.UsageDataService.enable";
	public static final String PREF_KEY_ASKED = "labrom.usage.UsageDataService.asked";
	
	private UUID guid;
	private String url;
	
	private static final int ITEMS_THRESHOLD = 50;
	private static final int TICKS_THRESHOLD = 6;
	private static final int INTERVAL_SECONDS = 15;
	int nTicks;

	private static final int MAX_ITEMS_IN_QUEUE = 600;
	private static final int MAX_ITEMS_IN_PAYLOAD = 200;
	Batch queue = new Batch(MAX_ITEMS_IN_QUEUE);
	long lastTick;
	long highestTimestampBeingSent;
	
	SharedPreferences prefs;

	private IBinder binder = new LocalBinder();
	
	final class LocalBinder extends Binder {
		UsageDataService getService() {
			return UsageDataService.this;
		}
	}
	@Override
	public IBinder onBind(Intent i) {
		return binder;
	}

	
	Handler handler = new Handler() {
		
		/**
		 * Handle messages sent from sender thread.
		 */
		public void handleMessage(android.os.Message msg) {
			// msg.what is supposed to be the number of items that were posted to the service, > 0 means basically success
			if(msg.what > 0 && highestTimestampBeingSent > 0) {
				queue.stripBeforeTimestamp(highestTimestampBeingSent);
			}
			
		}
	};
	
	@Override
	public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
		if(PREF_KEY_ENABLE.equals(key)) {
			if(isEnabled()) {
				Log.i(D.TAG, "Usage Data service is enabled, starting service");
				startService(new Intent(getApplicationContext(), UsageDataService.class));
			} else {
				Log.i(D.TAG, "Usage Data service is not enabled, stopping service");
				stopSelf();
			}
		}
	}
	
	
	
	/**
	 * Tells whether Usage Data service is enabled. Returns false if the service was not explicitly enabled
	 * using {@link #setEnabled(boolean)} with value {@code true}.
	 * The service needs to be enabled once only, this information is stored in application preferences.
	 * @return
	 */
	public boolean isEnabled() {
		return prefs.getBoolean(PREF_KEY_ENABLE, false);
	}
	
	/**
	 * Tells whether the user was asked whether to use the Usage Data service or not. Returns
	 * false until {@link #setEnabled(boolean)} is invoked regardless of the value.
	 * Once the user was asked, this method will always return true even if the service or application
	 * is restarted (this information is saved in application preferences).
	 * @return
	 */
	public boolean userWasAsked() {
		return prefs.getBoolean(PREF_KEY_ASKED, false);
		
	}
	
	/**
	 * Tells the service that the user was asked whether to enable the service or not. This information is
	 * then stored in application preferences so there's no need to ask the user every time the application
	 * or service is restarted.
	 * If the application offers a preferences option where the user can opt-in or opt-out of the service,
	 * this method can be used to update the saved user choice.
	 * If {@code enable == false} then the service is stopped. If {@code == true} and the service is not running,
	 * the service is NOT started automatically, it needs to be started by another component.
	 * @param enable
	 */
	public void setEnabled(boolean enable) {
		prefs.edit()
		.putBoolean(PREF_KEY_ASKED, true)
		.putBoolean(PREF_KEY_ENABLE, enable)
		.commit();
		// if enable == false service will be stopped when the callback on preference change is triggered
	}
	
	

	
	@Override
	public void onCreate() {

		int appVersion = 1;
		try {
			PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
			appVersion = info.versionCode;
			for(ServiceInfo service : info.services) {
				if(service.name.equals(getClass().getName()) && service.metaData != null) {
					url = service.metaData.getString("service.url");
					break;
				}
			}
		} catch (NameNotFoundException e) {
			Log.e(D.TAG, "Unable to read package info");
			e.printStackTrace();
		}
		if(url == null) {
			Log.e(D.TAG, "Unable to read service URL from service meta-data");
			return;
		}

		prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
		prefs.registerOnSharedPreferenceChangeListener(this);

		String guidstr = prefs.getString(PREF_KEY_GUID, null);
		if(guidstr == null) {
			guid = UUID.randomUUID();
			prefs.edit().putString(PREF_KEY_GUID, guid.toString()).commit();
		} else {
			guid = UUID.fromString(guidstr);
		}
		
		url = new StringBuilder(url)
		.append("/app").append(String.valueOf(appVersion))
		.append('/').append(guid.toString())
		.toString();
		
		Log.i(D.TAG, "Usage Data service initialization successful");
	}
	
	
	@Override
	public void onStart(Intent intent, int startId) {
		Log.i(D.TAG, "Usage Data service was started");
		if(!isEnabled()) {
			Log.i(D.TAG, "Usage Data service is not enabled, exiting");
			return;
		}
		
		if(url == null) {
			Log.w(D.TAG, "No service URL was configured, exiting");
			return;
		}

		Runnable r = new Runnable() {
			public void run() {
				tick();
				handler.postDelayed(this, INTERVAL_SECONDS * 1000);
			}
		};
		handler.postDelayed(r, INTERVAL_SECONDS * 1000);
	}
	
	public UUID getAssignedGuid() {
		return guid;
	}
	
	void tick() {
		// Ensure interval between ticks
		long now = System.currentTimeMillis();
		if(lastTick + INTERVAL_SECONDS * 1000 > now)
			return;
		lastTick = now;
		nTicks ++;
		if(shouldSend()) {
			nTicks = 0;
			Log.i(D.TAG, "Sending usage data: " + queue.size() + " items");
			send();
		}
	}

	boolean shouldSend() {
		if(nTicks >= TICKS_THRESHOLD && !queue.isEmpty()) {
			Log.d(D.TAG, "Should send because " + TICKS_THRESHOLD + " ticks have occurred and queue size is " + queue.size());
			return true;
		}
			
		if(queue.size() >= ITEMS_THRESHOLD) {
			Log.d(D.TAG, "Should send because queue size is " + queue.size() + ", which is >= " + ITEMS_THRESHOLD);
			return true;
		}
		
		Log.d(D.TAG, "Should NOT send, ticks: " + nTicks + ", queue size: " + queue.size());
		return false;
	}
	
	void send() {
		Batch beingSent = queue.peek(MAX_ITEMS_IN_PAYLOAD);
		Log.i(D.TAG, "Peeked " + beingSent.size() + " items out of " + queue.size() + " to be sent over");
		highestTimestampBeingSent = beingSent.getLast().getTimestamp();
		Sender sender = new Sender(handler, beingSent, url);
		sender.start();
	}
	
	
	/**
	 * Adds a Usage Data Item that has a type ID only.
	 * @param typeId the item type ID, must be positive, max value is 999999
	 */
	public void addItem(int typeId) {
		addItem(0, typeId, 0);
	}
	
	/**
	 * Adds a Usage Data Item that has a screen ID, type ID and a parameter value.
	 * @param screenId an application screen ID where the event occurred, must be positive, max value is 999999
	 * @param typeId the item type ID, must be positive, max value is 999999
	 * @param parameter any parameter value used to specialize the item's meaning, must be positive, max value is 999999
	 */
	public void addItem(int screenId, int typeId, int parameter) {
		if(!isEnabled()) // TODO Do we need some caching here, to avoid hitting the preferences storage every time?
			return;
		
		if(screenId < 0 || screenId > 999999)
			throw new IllegalArgumentException("Must have 0 <= screenId <= 999999");
		if(typeId < 0 || typeId > 999999)
			throw new IllegalArgumentException("Must have 0 <= typeId <= 999999");
		if(parameter < 0 || parameter > 999999)
			throw new IllegalArgumentException("Must have 0 <= parameter <= 999999");
		
		long type = parameter;
		type += typeId * 1000000;
		type += screenId * 1000000000000l;
		Udi u = new Udi();
		u.setTimestampNow();
		u.setTypeId(type);
		queue.add(u);
	}
		
}
